<?php

namespace app\admin\command;

use think\Config;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\Exception;
use think\Env;

require_once('/usr/share/nginx/html/application/common/library/esl/ESL.php');

class ASRDispatcher extends Command
{

    //重连时间
    protected $reconnectInterval = 1000; //单位：毫秒

    protected $sentenceInterval = 5000; //话术间隔停顿时间，单位：毫秒

    protected $noWaitInterval = 10; //单位：毫秒

    protected $breakInterval = 501; //命中关键字后的停顿间隔时间，单位：毫秒

    protected $sock = NULL;

    //信道
    protected $asrSessions = array();

    //命令行初始化
    protected function configure()
    {
        $this->setName('asrdispatcher')
            ->setDescription('ASR recognize dispatcher');
    }

    //随机播放一条语音
    protected function getRandomVoice($voices)
    {
        $voice_count = count($voices);
        if ($voice_count <= 0) {
            return;
        }
        return $voices[rand(0, $voice_count - 1)];
    }

    //记录制定信道的通话明细到json文件
    protected function recordResult($uuid, $voice, $type)
    {
        $res = array();
        $res['type'] = $type;
        $voice['time'] = time();
        if (isset($voice['negative'])) {
            unset($voice['negative']);
        }
        $res['voice'] = $voice;
        $res['flow_name'] = '';
        if (!empty($this->asrSessions[$uuid]['last_flow_name'])) {
            $res['flow_name'] = $this->asrSessions[$uuid]['last_flow_name'];
        }
        array_push($this->asrSessions[$uuid]['execute_result'], $res);
        $jobid = $this->asrSessions[$uuid]['jobid'];
        $scriptoutputpath = $this->asrSessions[$uuid]['scriptoutputpath'];
        $outputpath = $scriptoutputpath . '/' . $jobid . '.json';
        //print_r($outputpath);
        $output_result = array('version' => '1.0', 'finished' => false, 'flow_result' => $this->asrSessions[$uuid]['execute_result']);
        file_put_contents($outputpath, json_encode($output_result, JSON_UNESCAPED_UNICODE));
        echo "\n";
    }

    //信道完成后回写json文件
    protected function markFinished($uuid)
    {
        if (empty($this->asrSessions[$uuid]['scriptoutputpath']) || empty($this->asrSessions[$uuid]['execute_result']) || empty($this->asrSessions[$uuid]['jobid'])) return;
        $scriptoutputpath = $this->asrSessions[$uuid]['scriptoutputpath'];
        $jobid = $this->asrSessions[$uuid]['jobid'];
        $outputpath = $scriptoutputpath . '/' . $jobid . '.json';
        $output_result = array('version' => '1.0', 'finished' => true, 'starttime' => $this->asrSessions[$uuid]['starttime'], 'endtime' => time(), 'flow_result' => $this->asrSessions[$uuid]['execute_result']);
        file_put_contents($outputpath, json_encode($output_result, JSON_UNESCAPED_UNICODE));
        exec("/usr/bin/php /usr/share/nginx/html/think analyzejob {$jobid} 1>/dev/null 2>&1 &");
    }

    //播放最后一个引导流程
    protected function playback_last_flow($uuid) {
        $script = $this->asrSessions[$uuid]['script'];
        $flow_count = count($script['flow']);
        if (empty($script['flow'][$flow_count])) {
            //hangup
            $this->hangup($uuid);
            return false;
        }
        $this->asrSessions[$uuid]['step'] = $flow_count;
        $flow = $script['flow'][$flow_count];
        $this->asrSessions[$uuid]['last_flow'] = $flow;
        $flow_voice = $this->getRandomVoice($flow['voice']);
        $this->asrSessions[$uuid]['last_flow_name'] = $flow['name'];
        $this->asrSessions[$uuid]['last_voice'] = $flow_voice;
        $this->pause($uuid);
        $this->playback_break_silence($uuid);
        $this->playback($uuid, $flow_voice);
        return true;
    }

    //播放最后一个挽回流程
    protected function playback_last_flow_negative($uuid) {
        $script = $this->asrSessions[$uuid]['script'];
        $flow_count = count($script['flow']);
        if (empty($script['flow'][$flow_count])) {
            //hangup
            $this->hangup($uuid);
            return false;
        }
        $this->asrSessions[$uuid]['step'] = $flow_count;
        $flow = $script['flow'][$flow_count];
        $this->asrSessions[$uuid]['last_flow'] = $flow;
        $flow_voice = $this->getRandomVoice($flow['voice']);
        if (empty($flow_voice['negative'])) {
            //hangup
            $this->hangup($uuid);
            return false;
        }
        $flow_voice = $flow_voice['negative'];
        $this->asrSessions[$uuid]['last_voice'] = $flow_voice;
        $this->asrSessions[$uuid]['last_flow_name'] = $flow['name'] . '_negative';
        $this->pause($uuid);
        $this->playback_break_silence($uuid);
        $this->playback($uuid, $flow_voice);
        return true;
    }

    //播放打断间隔静音
    protected function playback_break_silence($uuid) {
        $cmd = "uuid_broadcast {$uuid} silence_stream://{$this->breakInterval}\n";
        print_r($cmd);
        $this->sock->api($cmd);
        $this->asrSessions[$uuid]['last_playback_time'] = time();
        $this->asrSessions[$uuid]['is_break_time'] = false;
    }

    //播放指定的语音文件到指定信道
    protected function playback($uuid, $voice)
    {
        if (!empty($voice['path'])) {
            $voice['path'] = str_replace('{$job_id}', $this->asrSessions[$uuid]['jobid'], $voice['path']);
            $voice['path'] = str_replace('{$task_id}', $this->asrSessions[$uuid]['taskid'], $voice['path']);
        }
        if ((!empty($voice['path'])) && (file_exists($voice['path']))) {
            $cmd = "uuid_broadcast {$uuid} {$voice['path']}\n";
        } else {
            $cmd = "uuid_broadcast {$uuid} say:unimrcp:xiaofang:{$voice['hint']}\n";
        }
        print_r($cmd);
        $this->sock->api($cmd);
        if (isset($voice['nobreak'])) {
            $cmd = "uuid_broadcast {$uuid} silence_stream://{$this->noWaitInterval}\n";
        } else {
            $cmd = "uuid_broadcast {$uuid} silence_stream://{$this->sentenceInterval}\n";
        }
        print_r($cmd);
        $this->sock->api($cmd);
        if (!empty($this->asrSessions[$uuid])) {
            $this->asrSessions[$uuid]['last_playback_time'] = time();
            $this->asrSessions[$uuid]['is_break_time'] = false;
        }
        $this->recordResult($uuid, $voice, 'robot');
    }

    //转接电话
    protected function transfer($uuid, $destination, $gateway = null, $register = null) {
        if (empty($destination)) {
            return;
        }        
        $cmd = '';
        $script = $this->asrSessions[$uuid]['script'];
        $play_wait_voice = '';
        if (!empty($script['transfer']['voice'])) {
            $voice = $script['transfer']['voice'];
            $play_wait_voice = "playback:{$voice['path']},";
            $this->recordResult($uuid, $voice, 'robot');
        }
        if (empty($gateway)) {
            $cmd = "uuid_transfer {$uuid} '{$play_wait_voice},transfer:{$destination} XML default' inline\n";
        } else {
            $cmd = "uuid_transfer {$uuid} '{$play_wait_voice}set:gateway={$gateway},set:sip_h_X-Gateway={$gateway},set:sip_h_X-Gateway-Register={$register},transfer:{$destination} XML outcall' inline\n";
        }
        print_r($cmd);
        $this->sock->api($cmd);

    }

    //暂停指定信道播放的语音
    protected function pause($uuid)
    {
        $cmd = "uuid_break {$uuid} all\n";
        print_r($cmd);
        $this->sock->api($cmd);
    }

    //杀死指定的信道
    protected function hangup($uuid)
    {
        $cmd = "uuid_kill {$uuid}\n";
        print_r($cmd);
        $this->sock->api($cmd);
    }

    //将用户记录到白名单(文件操作)
    protected function appendWhitelist($callerid, $hwords, $sentence)
    {
        $filename = "/usr/share/nginx/html/whitelist/whitelist.txt";
        $handle = fopen($filename, "a+");
        $line = "{$callerid}:{$hwords},{$sentence}\n";
        fwrite($handle, $line);
        fclose($handle);
    }

    //白名单验证
    protected function checkHarasswords($uuid, $sentence)
    {
        $harass_words = Env::get('harass.words', '');
        if (empty($harass_words)) return false;
        $harasswords = explode(',', $harass_words);
        foreach ($harasswords as $hwords) {
            if (strpos($sentence, $hwords) !== false) {
                if (!empty($this->asrSessions[$uuid]['callerid'])) {
                    $this->appendWhitelist($this->asrSessions[$uuid]['callerid'], $hwords, $sentence);
                }
                return true;
            }
        }
        return false;
    }

    //关键词匹配
    protected function compareKeywords($uuid, $sentence)
    {
        $script = $this->asrSessions[$uuid]['script'];
        if (empty($script['faq'])) return;
        $faq = $script['faq'];
        foreach ($faq as $one) {
            $keywords = explode(',', $one['keywords']);
            foreach ($keywords as $kwords) {
                if (empty($kwords)) continue;
                if (strpos($sentence, $kwords) !== false) {
                    $one['hit'] = $kwords;
                    $one['hit_type'] = 'normal';
                    return $one;
                }
            }
            $keywords = empty($one['associated_keywords']) ? [] : explode(',', $one['associated_keywords']);
            foreach ($keywords as $kwords) {
                if (empty($kwords)) continue;
                if (strpos($sentence, $kwords) !== false) {
                    $one['hit'] = $kwords;
                    $one['hit_type'] = 'associated';
                    return $one;
                }
            }
            $keywords = empty($one['misrecognize_keywords']) ? [] : explode(',', $one['misrecognize_keywords']);
            foreach ($keywords as $kwords) {
                if (empty($kwords)) continue;
                if (strpos($sentence, $kwords) !== false) {
                    $one['hit'] = $kwords;
                    $one['hit_type'] = 'misrecognize';
                    return $one;
                }
            }
        }
    }

    //相关词，模糊词校测
    protected function checkPositive($uuid, $sentence)
    {
        $positive_words = $this->asrSessions[$uuid]['script']['positive_words'];
        $negative_words = $this->asrSessions[$uuid]['script']['negative_words'];
        foreach ($negative_words as $one) {
            if (strpos($sentence, $one) !== false) {
                return false;
            }
        }
        foreach ($positive_words as $one) {
            if (strpos($sentence, $one) !== false) {
                return true;
            }
        }
        return null;
    }

    //语音识别处理函数
    protected function processWords($uuid, $sentence)
    {
        if ((empty($sentence)) && ($this->asrSessions[$uuid]['is_break_time'] == true)) {
            if ($this->asrSessions[$uuid]['r_u_ok'] == false) {
                $this->hangup($uuid);
                return;
            }
            $is_last_flow = false;
            if (!empty($this->asrSessions[$uuid]['step'])) {
                $script = $this->asrSessions[$uuid]['script'];
                if (empty($script['flow'][$this->asrSessions[$uuid]['step'] + 1])) {
                    $is_last_flow = true;
                }
            }
            if (($this->asrSessions[$uuid]['mute_count'] >= 2) && (!$is_last_flow)) {
                $this->asrSessions[$uuid]['mute_count'] = 0;
                $this->asrSessions[$uuid]['r_u_ok'] = false;
                $voice = array('hint' => '您好，您还在吗？', 'path' => '/usr/share/nginx/html/public/uploads/silence_prompt_voice.wav');
                if (!empty($this->asrSessions[$uuid]['script']['silence_prompt_voice'])) {
                    $voice = $this->asrSessions[$uuid]['script']['silence_prompt_voice'];
                }
                $this->playback($uuid, $voice);
                return;
            }
            $this->asrSessions[$uuid]['mute_count']++;
        }
        if (!empty($sentence)) {
            $this->asrSessions[$uuid]['r_u_ok'] = true;
            $this->asrSessions[$uuid]['mute_count'] = 0;
        }
        //$voice = array("hint" => $sentence);
        //$this->playback($uuid, $voice);
        if ((!empty($sentence)) && (!empty($this->asrSessions[$uuid]['meet_harass_words']))) {
            return;
        }
        if ($this->checkHarasswords($uuid, $sentence)) {
            $this->asrSessions[$uuid]['meet_harass_words'] = true;
            $script = $this->asrSessions[$uuid]['script'];
            $flow_count = count($script['flow']);
            if (empty($script['flow'][$flow_count])) {
                //hangup
                $this->hangup($uuid);
                return;
            }
            $this->asrSessions[$uuid]['step'] = $flow_count;
            $flow = $script['flow'][$flow_count];
            $this->asrSessions[$uuid]['last_flow'] = $flow;
            $flow_voice = $this->getRandomVoice($flow['voice']);;
            if (empty($flow_voice['negative'])) {
                $this->asrSessions[$uuid]['last_flow_name'] = $flow['name'];
            } else {
                $this->asrSessions[$uuid]['last_flow_name'] = $flow['name'] . '_negative';
                $flow_voice = $flow_voice['negative'];
            }
            $this->asrSessions[$uuid]['last_voice'] = $flow_voice;
            $this->pause($uuid);
            $this->playback_break_silence($uuid);
            $this->playback($uuid, $flow_voice);
            return;
        }
        $faq = $this->compareKeywords($uuid, $sentence);
        if (!empty($faq['voice'])) {
            $faq_voice = $this->getRandomVoice($faq['voice']);
            $faq_voice['keywords'] = $faq['keywords'];
            $faq_voice['hit'] = $faq['hit'];
            $faq_voice['hit_type'] = $faq['hit_type'];
            //print_r($faq_voice);
            if (!empty($faq_voice)) {
                $this->asrSessions[$uuid]['last_voice'] = $faq_voice;
                $this->asrSessions[$uuid]['last_flow_name'] = 'faq';
                $this->pause($uuid);
                $this->playback_break_silence($uuid);
                $this->playback($uuid, $faq_voice);
                return;
            }

        }
        $positive = $this->checkPositive($uuid, $sentence);
        if ($positive === false) {
            if ('faq_negative' == $this->asrSessions[$uuid]['last_flow_name']) {
                $this->playback_last_flow_negative($uuid);
                return;
            }
            if (!empty($this->asrSessions[$uuid]['last_voice']['negative'])) {
                $negative_voice = $this->asrSessions[$uuid]['last_voice']['negative'];
                $this->asrSessions[$uuid]['last_voice'] = $negative_voice;
                if (!empty($this->asrSessions[$uuid]['last_flow_name'])) {
                    $this->asrSessions[$uuid]['last_flow_name'] = $this->asrSessions[$uuid]['last_flow_name'] . '_negative';
                }
                $this->pause($uuid);
                $this->playback_break_silence($uuid);
                $this->playback($uuid, $negative_voice);
                return;
            }
        } else if ($positive === true) {
            if ('faq' == $this->asrSessions[$uuid]['last_flow_name']) {
                $script = $this->asrSessions[$uuid]['script'];
                if ((!empty($script['transfer']['destination'])) && (!empty($script['transfer']['gateway'])) && (!empty($script['transfer']['register']))) {
                    $this->transfer($uuid, $script['transfer']['destination'], $script['transfer']['gateway'], $script['transfer']['register']);
                    return;
                } else if (!empty($script['transfer']['destination'])) {
                    $this->transfer($uuid, $script['transfer']['destination']);
                    return;
                }
            }
            if (('faq' == $this->asrSessions[$uuid]['last_flow_name']) || ('faq_negative' == $this->asrSessions[$uuid]['last_flow_name'])) {
                $this->playback_last_flow($uuid);
                return;
            }
        }
        if ($this->asrSessions[$uuid]['is_break_time'] == false) {
            //if (!empty($sentence)) {
            return;
            //}
        }
        $this->asrSessions[$uuid]['step'] = $this->asrSessions[$uuid]['step'] + 1;
        $script = $this->asrSessions[$uuid]['script'];
        if (empty($script['flow'][$this->asrSessions[$uuid]['step']])) {
            //hangup
            $this->hangup($uuid);
            return;
        }
        $flow = $script['flow'][$this->asrSessions[$uuid]['step']];
        $this->asrSessions[$uuid]['last_flow'] = $flow;
        $this->asrSessions[$uuid]['last_flow_name'] = $flow['name'];

        $flow_voice = $this->getRandomVoice($flow['voice']);
        if (empty($flow_voice)) {
            return;
        }
        $this->asrSessions[$uuid]['last_voice'] = $flow_voice;
        $this->pause($uuid);
        $this->playback($uuid, $flow_voice);
    }

    //main函数
    protected function execute(Input $input, Output $output)
    {
        while (1) {
            $this->sock = new \ESLconnection('127.0.0.1', '8021', 'ClueCon');
            $this->sock->sendRecv("event json CUSTOM asr");
            $this->sock->sendRecv("event json CHANNEL_HANGUP_COMPLETE");
            $this->sock->sendRecv("event json CHANNEL_EXECUTE_COMPLETE");
            if ($this->sock->connected()) {
                echo "ESL Connected!\n";
                while ($this->sock->connected()) {
                    $this->doLoop();
                }
            } else {
                $this->asrSessions = null;
                $this->asrSessions = array();
                usleep($this->reconnectInterval * 1000);
                echo "Reconnecting ESL ...\n";
            }
        }
    }

    //消息循环体
    protected function doLoop()
    {
        try {
            $event = $this->sock->recvEvent();
            //print_r($event->serialize('json'));
            $evt = $event->serialize('json');
            $evt = json_decode($evt, true);
            $channel_uuid = $evt['Channel-Call-UUID'];
            //挂机
            if ($evt['Event-Name'] == 'CHANNEL_HANGUP_COMPLETE') {
                if (isset($this->asrSessions[$channel_uuid])) {
                    $this->markFinished($channel_uuid);
                    $this->asrSessions[$channel_uuid] = null;
                    unset($this->asrSessions[$channel_uuid]);
                    $session_count = count($this->asrSessions);
                    /*if ($session_count <= 0) {
                        gc_collect_cycles();
                    }*/
                    echo "Remove session [{$channel_uuid}], current session count: {$session_count}\n";
                }
            } else if ($evt['Event-Name'] == 'CHANNEL_EXECUTE_COMPLETE') {
                if (empty($evt['Application'])
                    || $evt['Application'] != 'playback') {
                    return;
                }
                if (!empty($evt['Application-Response'])
                    && ($evt['Application-Response']) == 'FILE PLAYED'
                    && !empty($evt['Application-Data'])
                    && (strpos($evt['Application-Data'], "silence_stream://{$this->breakInterval}") === false)
                    && (strpos($evt['Application-Data'], "silence_stream://{$this->noWaitInterval}") === false)
                    && (strpos($evt['Application-Data'], "silence_stream://{$this->sentenceInterval}") === false)) {
                    $this->asrSessions[$channel_uuid]['is_break_time'] = true;
                }
                if (empty($evt['Application-Response'])
                    || ($evt['Application-Response']) != 'FILE PLAYED'
                    || empty($evt['Application-Data'])) {
                    return;
                } else {
                    if ((strpos($evt['Application-Data'], "silence_stream://{$this->sentenceInterval}") !== false)
                        || (strpos($evt['Application-Data'], "silence_stream://{$this->noWaitInterval}") !== false)) {
                    } else {
                        return;
                    }
                }
                if (empty($this->asrSessions[$channel_uuid])) {
                    return;
                }
                echo "Playback complete!\n";
                //print_r($evt);
                echo "\n";
                $this->processWords($channel_uuid, '');
            } else if ($evt['ASR-Event'] == 'Start') {
                gc_collect_cycles();
                $real_mem_usage = memory_get_usage(true);
                $total_mem_usage = memory_get_usage(false);
                echo "\nMemory Usage(real): {$real_mem_usage} , (total): {$total_mem_usage}\n";
                //print_r($channel_uuid);echo "\n";
                $session = array();
                $session['mute_count'] = 1;
                $session['r_u_ok'] = true;
                $session['channel_uuid'] = $channel_uuid;
                $session['starttime'] = time();
                $session['updatetime'] = time();
                $session['callerid'] = empty($evt['ASR-Callerid']) ? '' : $evt['ASR-Callerid'];
                $session['calleeid'] = empty($evt['ASR-Calleeid']) ? '' : $evt['ASR-Calleeid'];
                $session['scriptid'] = $evt['ASR-Script-Id'];
                $session['jobid'] = empty($evt['ASR-Job-Id']) ? '' : $evt['ASR-Job-Id'];
                $session['taskid'] = empty($evt['ASR-Task-Id']) ? '' : $evt['ASR-Task-Id'];
                $session['scriptpath'] = empty($evt['ASR-Speech-Script-Path']) ? '/usr/share/nginx/html/speech_script' : $evt['ASR-Speech-Script-Path'];
                $session['scriptoutputpath'] = empty($evt['ASR-Speech-Script-Output-Path']) ? '/usr/share/nginx/html/speech_script_output' : $evt['ASR-Speech-Script-Output-Path'];

                $action_json = file_get_contents($session['scriptpath'] . '/' . $session['scriptid'] . '.json');
                $session['script'] = json_decode($action_json, true);

                $session['step'] = 1;
                $session['is_break_time'] = false;

                $session['last_flow'] = $session['script']['flow'][1];
                $session['last_flow_name'] = $session['script']['flow'][1]['name'];

                //$this->transfer($channel_uuid, '1000');return;
                //$this->transfer($channel_uuid, '17751725757', '15306526320', 'receive');

                $voice = $this->getRandomVoice($session['script']['flow'][1]['voice']);
                $session['last_voice'] = $voice;

                $session['execute_result'] = array();

                $this->asrSessions[$channel_uuid] = $session;

                $this->playback($channel_uuid, $voice);

            } else if ($evt['ASR-Event'] == 'SentenceBegin') {
                print_r($evt['ASR-Response']);
                echo "\r\n";
                if ($this->asrSessions[$channel_uuid]['is_break_time']) {
                    if (empty($this->asrSessions[$channel_uuid]['meet_harass_words'])) {
                        $this->pause($channel_uuid);
                    }
                }
                return;
            } else if ($evt['ASR-Event'] == 'SentenceEnd') {
                print_r($evt['ASR-Response']);
                echo "\n\n";
                if (empty($this->asrSessions[$channel_uuid])) {
                    return;
                }
                $asr_res = json_decode($evt['ASR-Response'], true);
                //if (!empty($asr_res['result']) && (!empty($asr_res['result']['text'])) && (!empty($asr_res['result']['end_time']))) {
                if (!empty($asr_res['payload']) && (!empty($asr_res['payload']['result']))) {
                    //if ($asr_res['result']['end_time'] < 0) {
                    //    if (time() - $this->asrSessions[$channel_uuid]['last_playback_time'] < 2){
                    //return;
                    //    }
                    //    //$this->pause($channel_uuid);
                    //    return;
                    //}
                    $words = $asr_res['payload']['result'];
                    echo "ASR - words: {$words} \n";
                    $this->recordResult($channel_uuid, array('hint' => $words), 'human');
                    if (time() - $this->asrSessions[$channel_uuid]['last_playback_time'] < 2) {
                        //return;
                    }
                    //$this->pause($channel_uuid);
                    //$this->recordResult($channel_uuid, array('hint' => $asr_res['result']['text']), 'human');
                    $this->processWords($channel_uuid, $words);
                } else {
                    $this->processWords($channel_uuid, '');
                    return;
                }
            }
        } catch (Exception $e) {
            return;
        }
    }
}
